<!DOCTYPE html>

<html>

<head>
    <title>Curvy Copter Plus</title>
    <style>
        body {
            /* set margin to 0 and overflow to hidden, to go fullscreen */
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script src="./ex_lib/dat.gui.min.js"></script>
    <script type="importmap">
        {
            "imports": {
              "three": "./ex_lib/three/three.module.js"
            }
          }
    </script>
</head>
<body>

<div id="Stats-output">
</div>
<!-- Div which will hold the Output -->
<div id="WebGL-output">
</div>

<script type="module">
    import * as THREE from 'three'
    import {OrbitControls} from './ex_lib/three/jsm/controls/OrbitControls.js'
    import {CurvyCopterPlus, DragControl, SceneUtils} from "./bundle_es-obfuscated.js";

    // once everything is loaded, we run our Three.js stuff.
    function init() {
        document.addEventListener('mousedown', onDocumentMouseDown, false);
        document.addEventListener('mousemove', onDocumentMouseMove, false);
        document.addEventListener('mouseup', onDocumentMouseUp, false);

        // create a scene, that will hold all our elements such as objects, cameras and lights.
        let scene = new THREE.Scene();
        // show axes in the screen
        let axes = new THREE.AxesHelper(20);
        scene.add(axes);

        let lights = [];
        lights[ 0 ] = new THREE.PointLight( 0xffffff, 1, 0 );
        lights[ 1 ] = new THREE.PointLight( 0xffffff, 1, 0 );
        lights[ 2 ] = new THREE.PointLight( 0xffffff, 1, 0 );
        lights[ 3 ] = new THREE.PointLight( 0xffffff, 1, 0 );

        scene.add( lights[ 0 ] );
        scene.add( lights[ 1 ] );
        scene.add( lights[ 2 ] );
        scene.add( lights[ 3 ] );

        let spotLight = new THREE.SpotLight(0xffffff);
        spotLight.position.set(-0, 5, 5);
        spotLight.castShadow = true;
        spotLight.intensity = 0.3;
        scene.add(spotLight);

        let SCREEN_WIDTH  = window.innerWidth;
        let SCREEN_HEIGHT = window.innerHeight;
        // create a render and set the size
        let webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
        webGLRenderer.setClearColor( 0x000000, 1 );
        webGLRenderer.setSize(window.innerWidth, window.innerHeight);
        webGLRenderer.autoClear = false;

        let autoOpLst = [], revertedOpLst, autoOpLstBackUp, autoPlayIndex = 0;
        let pivot = new THREE.Object3D();
        scene.add(pivot);

        let builder = CurvyCopterPlus();
        let parts = builder.createParts();
        parts.forEach(part => scene.add(part));
        let raycastingNeeded = builder.getRaycastingNeeded();
        let bounding = builder.getBounding();
        scene.add(bounding);

        function recreatePuzzle(newParts) {
            parts.forEach(part => scene.remove(part));
            parts = newParts;
            parts.forEach(part => scene.add(part));
        }

        let camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
        camera.position.z = 6;
        camera.position.x = 6;
        camera.position.y = 6;
        let trackballControls = new OrbitControls(camera, webGLRenderer.domElement);

        trackballControls.rotateSpeed = 1.0;
        trackballControls.zoomSpeed = 1.0;
        trackballControls.panSpeed = 1.0;
        trackballControls.staticMoving = true;

        let origin = new THREE.Vector3(0, 0, 0);
        let cameraOppo = new THREE.PerspectiveCamera().copy(camera);

        let camera1 = new THREE.PerspectiveCamera().copy(camera);
        let camera2 = new THREE.PerspectiveCamera().copy(camera);
        let camera3 = new THREE.PerspectiveCamera().copy(camera);

        // add the output of the renderer to the html element
        document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

        this.getScene = function () {
            return scene;
        };
        this.isAutoRotating = function () {
            return layerRotating;
        };
        this.getCameraControl = function () {
            return trackballControls;
        };
        this.getIntersects = function (x, y, singleUnit) {
            let vector = new THREE.Vector3(( x / window.innerWidth ) * 2 - 1, -( y / window.innerHeight ) * 2 + 1, 0.5);
            vector = vector.unproject(camera);

            let raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
            if (singleUnit == null) {
                return raycaster.intersectObjects(raycastingNeeded);
            }
            return raycaster.intersectObjects([singleUnit]);
        };
        this.getPivot = function () {
            return pivot;
        };
        this.builder = builder;
        let dragControl = DragControl(this);

        let layerRotating = false, rotatingIndex = 0, rotateStep, rotateParts, rotatingTime, rotateAxis, quaternion;
        function applyRotation(operation) {
            rotateAxis = builder.getRotateAxisBy(operation);
            let rotation = builder.getRotation(operation);
            rotatingTime = Math.round(Math.abs(rotation/Math.PI) * controls.rotatePiDuration);

            rotateParts = builder.getParts(operation);
            if (rotateParts.length == 0) {
                console.log("error, invalid operation");
                return false;
            }
            layerRotating = true;
            rotateStep = rotation / rotatingTime;
            rotatingIndex = 0;
            quaternion = new THREE.Quaternion();
            quaternion.setFromAxisAngle( rotateAxis.normalize(), rotateStep);
            pivot.rotation.set( 0, 0, 0 );
            pivot.updateMatrixWorld();
            rotateParts.forEach(part => {
                SceneUtils.attach( part, scene, pivot )
            } );
            return true;
        }

        function onDocumentMouseDown(event) {
            dragControl.mouseDown(event);
        }
        function onDocumentMouseMove(event) {
            dragControl.mouseMove(event);
        }
        function onDocumentMouseUp(event) {
            dragControl.mouseUp(event);
        }

        let controls = new function () {
            let reverted = false;
            this.randomRotate = function () {
                if (autoOpLst && autoPlayIndex < autoOpLst.length) {
                    return;
                }
                reverted = false;
                autoOpLst = builder.getRandomOperations(controls.randomSteps);
                autoPlayIndex = 0;
                autoOpLstBackUp = autoOpLst.slice();
                revertedOpLst = builder.getRevertedOperations(autoOpLst);
            };
            this.switchRandomRotate = function () {
                console.log("-----------switch-----------")
                if (autoPlayIndex < autoOpLst.length) {
                    console.log("cannot revert random rotations, random rotating is in progress.")
                    return;
                }
                autoPlayIndex = 0;
                if (!reverted) {
                    reverted = true;
                    autoOpLst = revertedOpLst.slice();
                } else {
                    reverted = false;
                    autoOpLst = autoOpLstBackUp.slice();
                }
            };
            this.stepBackwards = function () {
                if (!controls.isPaused || layerRotating) {
                    console.log("cannot step backward, until rotate stopped");
                    return;
                }
                if (autoPlayIndex < 1) {
                    console.log("cannot step backward, already reach beginning");
                    return;
                }
                autoPlayIndex--;
                let op = autoOpLst[autoPlayIndex];
                let reverted = builder.getRevertOperation(op);
                applyRotation(reverted);
            };
            this.stepForwards = function () {
                if (!controls.isPaused || layerRotating) {
                    console.log("cannot step forward, until rotate stopped");
                    return;
                }
                if (autoPlayIndex > autoOpLst.length - 1) {
                    console.log("cannot step forward, already reach ending");
                    return;
                }
                let op = autoOpLst[autoPlayIndex];
                applyRotation(op);
                autoPlayIndex++;
            };
            this.serializeStatus = function () {
                controls.statusCode = builder.serializeStatus();
                console.log(controls.statusCode);
            };
            this.deSerialize = function () {
                if (controls.statusCode && controls.statusCode.length > 30) {
                    parts.forEach(part => scene.remove(part));
                    parts = builder.deSerializeStatus(controls.statusCode);
                    parts.forEach(part => scene.add(part));
                }
            };
            this.statusCode = "";
            this.isPaused = false;
            this.rotatePiDuration = 60;
            this.randomSteps = 36;
            this.size = builder.getSize();
            this.gap = builder.getGap();
            this.extraCameras = ["none", "opposite", "tetrahedron"];
            this.extraCamera = this.extraCameras[0];
            this.extraCameraType = 0;
        };

        let gui = new dat.GUI();
        gui.add(controls, 'randomRotate').name("random rotate");
        gui.add(controls, 'switchRandomRotate').name("switch random rotate");
        gui.add(controls, 'isPaused').name("pause auto rotate");
        gui.add(controls, 'stepBackwards').name("step backward");
        gui.add(controls, 'stepForwards').name("step forward");
        gui.add(controls, 'randomSteps', 9, 99).name("random steps:").step(1).onChange(function (e) {
            controls.randomSteps = e;
        });
        gui.add(controls, 'rotatePiDuration', 15, 150).name("PI duration").step(1).onChange(function (e) {
            controls.rotatePiDuration = e;
        });
        gui.add(controls, 'extraCamera', controls.extraCameras).name("extra camera").onChange(function (e) {
            let index = controls.extraCameras.indexOf(e);
            controls.extraCameraType = index;
        });
        let saveStatusGui = gui.addFolder("Serialize and Deserialize");
        saveStatusGui.add(controls, 'serializeStatus').name("serialize status");
        saveStatusGui.add(controls, 'statusCode').name("serialized:").listen();
        saveStatusGui.add(controls, 'deSerialize').name("deSerialize from text");

        let styleGui = gui.addFolder("CurvyCopterCube Style");
        styleGui.add(controls, 'size', 0.3, 2).onChange(function (e) {
            controls.size = e;
            builder.adjustGapAndPartSize({size: e});
        });
        styleGui.add(controls, 'gap', 0, 3).onChange(function (e) {
            controls.gap = e;
            builder.adjustGapAndPartSize({gap: e});
        });

        let tetrahedronPoints = [new THREE.Vector3(1, 1, 1), new THREE.Vector3(-1, -1, 1),
            new THREE.Vector3(-1, 1, -1), new THREE.Vector3(1, -1, -1)];
        let tetraAxis = new THREE.Vector3();
        let qua = new THREE.Quaternion();
        function updateLights(t1, t2, t3) {
            lights[0].position.copy(camera.position);
            lights[1].position.copy(t1);
            lights[2].position.copy(t2);
            lights[3].position.copy(t3);
        }

        function onCameraChange() {
            tetraAxis.crossVectors(tetrahedronPoints[0], camera.position).normalize();
            let angle = Math.acos( tetrahedronPoints[0].dot( camera.position ) / tetrahedronPoints[0].length() / camera.position.length() );
            qua.setFromAxisAngle( tetraAxis, angle );
            let scalar = camera.position.length() / tetrahedronPoints[0].length();
            let t1 = tetrahedronPoints[1].clone().applyQuaternion(qua).multiplyScalar(scalar);
            let t2 = tetrahedronPoints[2].clone().applyQuaternion(qua).multiplyScalar(scalar);
            let t3 = tetrahedronPoints[3].clone().applyQuaternion(qua).multiplyScalar(scalar);
            updateLights(t1, t2, t3);

            camera1.position.copy(t1); camera1.lookAt(origin);
            camera2.position.copy(t2); camera2.lookAt(origin);
            camera3.position.copy(t3); camera3.lookAt(origin);

            cameraOppo.position.copy(camera.position);
            cameraOppo.position.negate();
            cameraOppo.lookAt(origin);
        }
        onCameraChange();
        trackballControls.addEventListener("change", function () {
            onCameraChange();
        });

        render();

        function render() {
            if (!layerRotating && autoOpLst && autoPlayIndex < autoOpLst.length && !controls.isPaused) {
                let autoOp = autoOpLst[autoPlayIndex];
                if(applyRotation(autoOp)) {
                    autoPlayIndex++;
                } else {
                    autoPlayIndex = autoOpLst.length;
                }
            }
            if (layerRotating) {
                rotatingIndex++;
                pivot.applyQuaternion(quaternion);
                if (rotatingIndex >= rotatingTime) {
                    layerRotating = false;
                    pivot.updateMatrixWorld();
                    rotateParts.forEach(part => {
                        SceneUtils.detach( part, pivot, scene );
                    } );
                }
            }

            trackballControls.update();
            requestAnimationFrame(render);
            dragControl.beforeFrameDraw();

            webGLRenderer.clear();
            webGLRenderer.setViewport( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
            webGLRenderer.render( scene, camera);
            if (controls.extraCameraType == 1) {
                webGLRenderer.setViewport( 0, 0, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
                webGLRenderer.render(scene, cameraOppo);
            } else if (controls.extraCameraType == 2) {
                webGLRenderer.setViewport( SCREEN_WIDTH/3, 0, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
                webGLRenderer.render(scene, camera2);
                webGLRenderer.setViewport( 0, SCREEN_HEIGHT*2/3, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
                webGLRenderer.render(scene, camera1);
                webGLRenderer.setViewport( SCREEN_WIDTH*2/3, SCREEN_HEIGHT*2/3, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
                webGLRenderer.render(scene, camera3);
            }
        }

        window.addEventListener( 'resize', function () {
            SCREEN_WIDTH  = window.innerWidth;
            SCREEN_HEIGHT  = window.innerHeight;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();

            cameraOppo.aspect = camera.aspect;
            cameraOppo.updateProjectionMatrix();
            camera1.aspect = camera.aspect;
            camera1.updateProjectionMatrix();
            camera2.aspect = camera.aspect;
            camera2.updateProjectionMatrix();
            camera3.aspect = camera.aspect;
            camera3.updateProjectionMatrix();

            webGLRenderer.setSize( window.innerWidth, window.innerHeight );

        }, false );
    }
    window.onload = init;
</script>
</body>
</html>